package cscie160.project.atm;
import java.util.HashMap;
import java.util.ArrayList;
import java.rmi.RemoteException;
import java.rmi.Naming;
import java.rmi.server.UnicastRemoteObject;

import cscie160.project.bank.AccountInfo;
import cscie160.project.bank.BankServer;
import cscie160.project.bank.Commands;
/**
 * This class is the actual ATM.
 * that has a reference to an instance of a class Account and manipulates its balance.
 * <BR>An instance of ATMImpl will run on the server. 
 * @author Michael Melnikov 
*/
public class ATMImpl extends UnicastRemoteObject implements ATM
{
    /**
    * set of listeners to be mainteined
    */
    ArrayList <ATMListener> registeredClients;
    
    /**
    * bank server the Atm works with
    */   
    private BankServer bankServer;
    
    /**
    * cash on hand
    */
    private int atmCashBalance;

    /**
    * default amount cash on hand
    */
    final static int defaultAtmCashBalance = 500;    

    /**
    * 1-arg constructor creates ATM with no accounts.
    * Looks up for the bank server
    * @param bankServerPort int
    */   
    protected ATMImpl(int bankServerPort) throws RemoteException
    {
        this(bankServerPort, defaultAtmCashBalance);
    }
    
    /**
    * 1-arg constructor creates ATM with no accounts.
    * Looks up for the bank server
    * @param bankServerPort int
    * @param atmCashBalance int
    */   
    protected ATMImpl(int bankServerPort, int atmCashBalance) throws RemoteException
    {
        super();
        String serverLookupUrl = "//localhost:" + Integer.toString(bankServerPort) + "/bankserver";
        try
        {
            bankServer = (BankServer)Naming.lookup(serverLookupUrl);
        } catch (java.rmi.NotBoundException nbex)
        {
            System.out.println(nbex.getClass().getName() + "; message=" + nbex.getMessage());
        } catch (java.net.MalformedURLException mue)
        {
            System.out.println(mue.getClass().getName() + "; message=" + mue.getMessage());
        }
        this.atmCashBalance = atmCashBalance;
    }
    
    /**
    * deposits sum equal to its argument to the account
    * @param accountInfo AccountInfo   
    * @param amount int
    */
    public void deposit(AccountInfo accountInfo, int amount) throws ATMException, RemoteException
    {
        AccountInfo targetAccountInfo = null;
        TransactionNotification notification = new TransactionNotification(
            Commands.DEPOSIT, accountInfo, targetAccountInfo , amount);
        executeTransaction(notification);
    }
  
    /**
    * makes bank to deposit sum equal to its argument to the account
    * @param aInfo AccountInfo     
    * @param amount int
    */
    public void doDeposit(AccountInfo aInfo, int amount) throws ATMException, RemoteException
    {
        bankServer.deposit(aInfo, amount);
    }
 

    /**
    * withdraws sum equal to its argument to the account
    * @param accountInfo AccountInfo    
    * @param amount int
    */
    public void withdraw(AccountInfo accountInfo, int amount) throws ATMException, RemoteException
    {
        AccountInfo targetAccountInfo = null;
        TransactionNotification notification = new TransactionNotification(
            Commands.WITHDRAW, accountInfo, targetAccountInfo , amount);
        executeTransaction(notification);
    }   

    /**
    * makes bank to withdraw sum equal to its argument off the account
    * @param aInfo AccountInfo    
    * @param amount int    
    */    
    public void doWithdraw(AccountInfo aInfo, int amount) throws ATMException, RemoteException
    {
        bankServer.withdraw(aInfo, amount);
    }


    /**
    * gives balance of the account.
    * @param accountInfo AccountInfo    
    * @return int
    */
    public int getBalance(AccountInfo accountInfo) throws ATMException, RemoteException
    {
        AccountInfo targetAccountInfo = null;
        TransactionNotification notification = new TransactionNotification(
            Commands.BALANCE, accountInfo, targetAccountInfo , null);
        Integer balance = executeTransaction(notification);
        return ((balance == null) ? 0 : balance.intValue());
    } 
    
    /**
    * makes bank to return balance of the account.
    * @param aInfo AccountInfo 
    * @return int
    */
    private int doGetBalance(AccountInfo aInfo) throws ATMException, RemoteException
    {
        return bankServer.getBalance(aInfo);
    }
    
    /**
    * makes transfer fro 1st to 2nd account.
    * withdraws sum equal to 3d argument from account defined by the 1st argument and deposits
    * it onto account defined by the 2nd argument
    * @param sourceAccountInfo AccountInfo
    * @param targetAccountInfo AccountInfo
    * @param amount int
    */
    public boolean transfer(AccountInfo sourceAccountInfo, AccountInfo targetAccountInfo, int amount)
        throws ATMException, RemoteException
    {
        TransactionNotification notification = new TransactionNotification(
            Commands.TRANSFER, sourceAccountInfo, targetAccountInfo, amount);
        executeTransaction(notification);
        return true;
    }    

    /**
    * makes bank to transfer from 1st to 2nd account.
    * withdraws sum equal to 3d argument from account defined by the 1st argument and deposits
    * it onto account defined by the 2nd argument
    * @param sourceAccountInfo AccountInfo
    * @param destAccountInfo AccountInfo 
    * @param amount int
    */
    public boolean doTransfer(AccountInfo sourceAccountInfo, AccountInfo destAccountInfo, int amount)
        throws ATMException, RemoteException
    {
        doWithdraw(sourceAccountInfo, amount);
        try
        {
            doDeposit(destAccountInfo, amount);
        }
        catch (ATMException atmex)
        {
            doDeposit(sourceAccountInfo, amount);
            return false;
        }
        return true;
    }

    /**
    * allows Client to register itself with the ATM as an ATMListener
    * @param client ATMListener
    */        
    public boolean registerListener(ATMListener client)
         throws RemoteException
    {
        if (client == null)
            return false;
        if (registeredClients == null)
            registeredClients = new ArrayList <ATMListener>();
        registeredClients.add(client);
        return true;
    }

    /**
    * notifies all registered listeners of the transaction
    * @param notification TransactionNotification
    */
    public void notifyListeners(TransactionNotification notification)
        throws RemoteException
    {
        if (registeredClients == null)
            return;
        for(ATMListener cl : registeredClients)
        {
            cl.receiveTransactionNotification (notification);
        }
    }

    private Integer executeTransaction (TransactionNotification notification)
        throws ATMException, RemoteException
    {
        if (notification == null)
            return null;
        notifyListeners(notification);
            
        Commands command = notification.getCommand();
        boolean isAuthentic = checkAuthentication(notification);
        if (!isAuthentic)
        {
            throw new ATMException("Security Exception: Account info is not valid!");
        }
        
        boolean isPermitted = checkPermission(notification);
        if (!isPermitted)
        {
            throw new ATMException("Security Exception: operation " + command + " is prohibited!");
        }
        
        AccountInfo sourceAccountInfo = notification.getSourceAccountInfo();
        int sourceAccountNumber = sourceAccountInfo.getAccountNumber();
        AccountInfo targetAccountInfo = notification.getTargetAccountInfo();
        int targetAccountNumber = (targetAccountInfo == null) ? 0 : targetAccountInfo.getAccountNumber();
        int amount = (notification.getAmount() == null) ? 0 : notification.getAmount().intValue();
  
        Integer balance = null;

        switch (command)
        {
            case DEPOSIT:
                doDeposit(sourceAccountInfo, amount);
                //atmCashBalance += amount; //Not required because deposits are supposed to be checks
                break;
            case WITHDRAW:
                if (amount > atmCashBalance)
                    throw new ATMException(command + " is not possible: ATM Cash Balance exceeded!");
                doWithdraw(sourceAccountInfo, amount);
                atmCashBalance -= amount;
                break;
            case BALANCE:
                balance = new Integer(doGetBalance(sourceAccountInfo));
                break;
            case TRANSFER:
                doTransfer(sourceAccountInfo, targetAccountInfo, amount);
                break;
            default:
                break;
        }
        return balance;
    }
    
    /**
    * checks whether the account info provided is legitimate
    * @param notification TransactionNotification
    * @return boolean
    */
    private boolean checkAuthentication(TransactionNotification notification)
    {
        boolean isAuthentic = false;
        AccountInfo aInfo = notification.getSourceAccountInfo();
        try
        {
            if (aInfo != null)
                isAuthentic = bankServer.isAuthentic(aInfo);
            if  (!isAuthentic)
                return false;

            aInfo = notification.getTargetAccountInfo();
            if (aInfo != null)
                isAuthentic = bankServer.isAuthentic(aInfo);
        } catch (RemoteException rex)
        {
            System.out.println(rex.getClass().getName() + "; message=" + rex.getMessage());
            return false;
        }
        return isAuthentic;
    }
    
    /**
    * checks whether the account allows requested transaction
    * @param notification TransactionNotification
    * @return boolean
    */
    private boolean checkPermission(TransactionNotification notification)
    {  
        boolean isPermitted = false;
        Commands command = notification.getCommand();
        AccountInfo aInfo = notification.getSourceAccountInfo();
        try
        {
            if (aInfo != null)
            {
                boolean isAtomicCommand =
                    command == Commands.DEPOSIT || command == Commands.WITHDRAW || command == Commands.BALANCE;
                if (isAtomicCommand)
                    isPermitted = bankServer.isPermitted(aInfo, command);
                else if (command == Commands.TRANSFER)
                    isPermitted = bankServer.isPermitted(aInfo, Commands.WITHDRAW);
            }
            if  (!isPermitted)
                return false;

            aInfo = notification.getTargetAccountInfo();
            if (aInfo != null && command == Commands.TRANSFER)
                isPermitted = bankServer.isPermitted(aInfo, Commands.DEPOSIT);
        } catch (RemoteException rex)
        {
            System.out.println(rex.getClass().getName() + "; message=" + rex.getMessage());
            return false;
        }
        return isPermitted;
    }
}
